{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 허깅페이스 임베딩(HuggingFace Embeddings)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# API KEY를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API KEY 정보로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH08-Embeddings\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import warnings\n",
    "\n",
    "# 경고 무시\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "\n",
    "# ./cache/ 경로에 다운로드 받도록 설정\n",
    "os.environ[\"HF_HOME\"] = \"./cache/\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 샘플 데이터"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "texts = [\n",
    "    \"안녕, 만나서 반가워.\",\n",
    "    \"LangChain simplifies the process of building applications with large language models\",\n",
    "    \"랭체인 한국어 튜토리얼은 LangChain의 공식 문서, cookbook 및 다양한 실용 예제를 바탕으로 하여 사용자가 LangChain을 더 쉽고 효과적으로 활용할 수 있도록 구성되어 있습니다. \",\n",
    "    \"LangChain은 초거대 언어모델로 애플리케이션을 구축하는 과정을 단순화합니다.\",\n",
    "    \"Retrieval-Augmented Generation (RAG) is an effective technique for improving AI responses.\",\n",
    "]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**참고(Reference)**\n",
    "\n",
    "![](./images/top-ranked-embeddings.png)\n",
    "\n",
    "- [(출처) Kor-IR: 한국어 검색을 위한 임베딩 벤치마크](https://github.com/teddylee777/Kor-IR?tab=readme-ov-file)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## HuggingFace Endpoint Embedding\n",
    "\n",
    "`HuggingFaceEndpointEmbeddings` 는 내부적으로 InferenceClient를 사용하여 임베딩을 계산한다는 점에서 HuggingFaceEndpoint가 LLM에서 수행하는 것과 매우 유사합니다. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_huggingface.embeddings import HuggingFaceEndpointEmbeddings\n",
    "\n",
    "model_name = \"intfloat/multilingual-e5-large-instruct\"\n",
    "\n",
    "hf_embeddings = HuggingFaceEndpointEmbeddings(\n",
    "    model=model_name,\n",
    "    task=\"feature-extraction\",\n",
    "    huggingfacehub_api_token=os.environ[\"HUGGINGFACEHUB_API_TOKEN\"],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Document 임베딩은 `embed_documents()` 를 호출하여 생성할 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "# Document Embedding 수행\n",
    "embedded_documents = hf_embeddings.embed_documents(texts)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"[HuggingFace Endpoint Embedding]\")\n",
    "print(f\"Model: \\t\\t{model_name}\")\n",
    "print(f\"Dimension: \\t{len(embedded_documents[0])}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Document Embedding 수행\n",
    "embedded_query = hf_embeddings.embed_query(\"LangChain 에 대해서 알려주세요.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "len(embedded_query)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 유사도 계산\n",
    "\n",
    "**벡터 내적을 통한 유사도 계산**\n",
    "- 벡터 내적(dot product)을 사용하여 유사도를 계산합니다. \n",
    "\n",
    "- 유사도 계산 공식:\n",
    "\n",
    "$$ \\text{similarities} = \\mathbf{query} \\cdot \\mathbf{documents}^T $$\n",
    "\n",
    "#### 벡터 내적의 수학적 의미\n",
    "\n",
    "**벡터 내적 정의**\n",
    "\n",
    "벡터 $\\mathbf{a}$와 $\\mathbf{b}$의 내적은 다음과 같이 정의됩니다:\n",
    "$$ \\mathbf{a} \\cdot \\mathbf{b} = \\sum_{i=1}^{n} a_i b_i $$\n",
    "\n",
    "**코사인 유사도와의 관계**\n",
    "\n",
    "벡터 내적은 다음과 같은 성질을 가집니다.\n",
    "$$ \\mathbf{a} \\cdot \\mathbf{b} = \\|\\mathbf{a}\\| \\|\\mathbf{b}\\| \\cos \\theta $$\n",
    "\n",
    "여기서,\n",
    "- $\\|\\mathbf{a}\\|$와 $\\|\\mathbf{b}\\|$는 각각 벡터 $\\mathbf{a}$와 $\\mathbf{b}$의 크기(노름, Euclidean norm)입니다.\n",
    "- $\\theta$는 두 벡터 사이의 각도입니다.\n",
    "- $\\cos \\theta$는 두 벡터 사이의 코사인 유사도입니다.\n",
    "\n",
    "**벡터 내적의 유사도 해석**\n",
    "내적 값이 클수록 (양의 큰 값일수록),\n",
    "- 두 벡터의 크기($\\|\\mathbf{a}\\|$와 $\\|\\mathbf{b}\\|$)가 크고,\n",
    "- 두 벡터 사이의 각도($\\theta$)가 작으며 ($\\cos \\theta$가 1에 가까움),\n",
    "\n",
    "이는 두 벡터가 유사한 방향을 가리키고, 크기가 클수록 더 유사하다는 것을 의미합니다.\n",
    "\n",
    "**벡터의 크기(노름) 계산**\n",
    "\n",
    "Euclidean norm 정의\n",
    "벡터 $\\mathbf{a} = [a_1, a_2, \\ldots, a_n]$에 대해, Euclidean norm $\\|\\mathbf{a}\\|$는 다음과 같이 정의됩니다:\n",
    "$$ \\|\\mathbf{a}\\| = \\sqrt{a_1^2 + a_2^2 + \\cdots + a_n^2} $$"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "----\n",
    "\n",
    "query 와 embedding_document 간의 유사도 계산"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "# 질문(embedded_query): LangChain 에 대해서 알려주세요.\n",
    "np.array(embedded_query) @ np.array(embedded_documents).T"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sorted_idx = (np.array(embedded_query) @ np.array(embedded_documents).T).argsort()[::-1]\n",
    "sorted_idx"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"[Query] LangChain 에 대해서 알려주세요.\\n====================================\")\n",
    "for i, idx in enumerate(sorted_idx):\n",
    "    print(f\"[{i}] {texts[idx]}\")\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## HuggingFace Embeddings\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `intfloat/multilingual-e5-large-instruct`\n",
    "\n",
    "- [intfloat/multilingual-e5-large-instruct](https://huggingface.co/intfloat/multilingual-e5-large-instruct)\n",
    "- [intfloat/multilingual-e5-large](https://huggingface.co/intfloat/multilingual-e5-large)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_huggingface.embeddings import HuggingFaceEmbeddings\n",
    "\n",
    "model_name = \"intfloat/multilingual-e5-large-instruct\"\n",
    "# model_name = \"intfloat/multilingual-e5-large\"\n",
    "\n",
    "hf_embeddings = HuggingFaceEmbeddings(\n",
    "    model_name=model_name,\n",
    "    model_kwargs={\"device\": \"mps\"},  # cuda, cpu\n",
    "    encode_kwargs={\"normalize_embeddings\": True},\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%time\n",
    "# Document\n",
    "embedded_documents1 = hf_embeddings.embed_documents(texts)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"Model: \\t\\t{model_name}\")\n",
    "print(f\"Dimension: \\t{len(embedded_documents[0])}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "vscode": {
     "languageId": "plaintext"
    }
   },
   "source": [
    "## BGE-M3 임베딩\n",
    "\n",
    "아래의 옵션 중 에러가 발생할 수 있는 옵션 설명\n",
    "\n",
    "- `{\"device\": \"mps\"}`: GPU 대신 MPS를 사용하여 임베딩 계산을 수행합니다. (Mac 사용자)\n",
    "- `{\"device\": \"cuda\"}`: GPU를 사용하여 임베딩 계산을 수행합니다. (Linux, Windows 사용자, 단 CUDA 설치 필요)\n",
    "- `{\"device\": \"cpu\"}`: CPU를 사용하여 임베딩 계산을 수행합니다. (모든 사용자"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_huggingface import HuggingFaceEmbeddings\n",
    "\n",
    "model_name = \"BAAI/bge-m3\"\n",
    "model_kwargs = {\"device\": \"mps\"}\n",
    "encode_kwargs = {\"normalize_embeddings\": True}\n",
    "hf_embeddings = HuggingFaceEmbeddings(\n",
    "    model_name=model_name, model_kwargs=model_kwargs, encode_kwargs=encode_kwargs\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%time\n",
    "# Document\n",
    "embedded_documents = hf_embeddings.embed_documents(texts)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"Model: \\t\\t{model_name}\")\n",
    "print(f\"Dimension: \\t{len(embedded_documents[0])}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "embedded_query = hf_embeddings.embed_query(\"LangChain 에 대해서 알려주세요.\")\n",
    "embedded_documents = hf_embeddings.embed_documents(texts)\n",
    "\n",
    "# 질문(embedded_query): LangChain 에 대해서 알려주세요.\n",
    "np.array(embedded_query) @ np.array(embedded_documents).T\n",
    "\n",
    "sorted_idx = (np.array(embedded_query) @ np.array(embedded_documents).T).argsort()[::-1]\n",
    "\n",
    "print(\"[Query] LangChain 에 대해서 알려주세요.\\n====================================\")\n",
    "for i, idx in enumerate(sorted_idx):\n",
    "    print(f\"[{i}] {texts[idx]}\")\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### `FlagEmbedding` 을 활용하는 방식\n",
    "\n",
    "**참고**\n",
    "- [FlagEmbedding - BGE-M3 Usage](https://github.com/FlagOpen/FlagEmbedding/tree/master/FlagEmbedding/BGE_M3#usage)\n",
    "\n",
    "\n",
    "`FlagEmbedding` 에서 제공하는 세 가지 접근법을 조합하면, 더욱 강력한 검색 시스템을 구축할 수 있습니다.\n",
    "\n",
    "- Dense Vector: BGE-M3의 다국어, 다중 작업 능력을 기반으로 함\n",
    "- Lexical weight를 활용한 sparse embedding으로 정확한 단어 매칭을 수행\n",
    "- ColBERT의 multi-vector 접근법으로 문맥을 고려한 세밀한 매칭 수행"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# FlagEmbedding 설치\n",
    "!pip install -qU FlagEmbedding"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from FlagEmbedding import BGEM3FlagModel\n",
    "\n",
    "model_name = \"BAAI/bge-m3\"\n",
    "bge_embeddings = BGEM3FlagModel(\n",
    "    model_name, use_fp16=True\n",
    ")  # use_fp16을 True로 설정하면 약간의 성능 저하와 함께 계산 속도가 빨라집니다.\n",
    "\n",
    "bge_embedded = bge_embeddings.encode(\n",
    "    texts,\n",
    "    batch_size=12,\n",
    "    max_length=8192,  # 이렇게 긴 길이가 필요하지 않은 경우 더 작은 값을 설정하여 인코딩 프로세스의 속도를 높일 수 있습니다.\n",
    ")[\"dense_vecs\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bge_embedded.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"Model: \\t\\t{model_name}\")\n",
    "print(f\"Dimension: \\t{len(embedded_documents[0])}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from FlagEmbedding import BGEM3FlagModel\n",
    "\n",
    "bge_flagmodel = BGEM3FlagModel(\n",
    "    \"BAAI/bge-m3\", use_fp16=True\n",
    ")  # use_fp16을 True로 설정하면 약간의 성능 저하와 함께 계산 속도가 빨라집니다.\n",
    "bge_encoded = bge_flagmodel.encode(texts, return_dense=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# 결과 출력(행, 열)\n",
    "bge_encoded[\"dense_vecs\"].shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Sparse Embedding (Lexical Weight)\n",
    "\n",
    "Sparse embedding은 벡터의 대부분의 값이 0인 고차원 벡터를 사용하는 임베딩 방식입니다. Lexical weight를 활용한 방식은 단어의 중요도를 고려하여 임베딩을 생성합니다.\n",
    "\n",
    "**작동 방식**\n",
    "1. 각 단어에 대해 lexical weight(어휘적 가중치)를 계산합니다. 이는 TF-IDF나 BM25 같은 방법을 사용할 수 있습니다.\n",
    "2. 문서나 쿼리의 각 단어에 대해, 해당 단어의 lexical weight를 사용하여 sparse vector의 해당 차원에 값을 할당합니다.\n",
    "3. 결과적으로 문서나 쿼리는 대부분의 값이 0인 고차원 벡터로 표현됩니다.\n",
    "\n",
    "**장점**\n",
    "- 단어의 중요도를 직접적으로 반영할 수 있습니다.\n",
    "- 특정 단어나 구문을 정확히 매칭할 수 있습니다.\n",
    "- 계산이 상대적으로 빠릅니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bge_flagmodel = BGEM3FlagModel(\n",
    "    \"BAAI/bge-m3\", use_fp16=True\n",
    ")  # use_fp16을 True로 설정하면 약간의 성능 저하와 함께 계산 속도가 빨라집니다.\n",
    "bge_encoded = bge_flagmodel.encode(texts, return_sparse=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lexical_scores1 = bge_flagmodel.compute_lexical_matching_score(\n",
    "    bge_encoded[\"lexical_weights\"][0], bge_encoded[\"lexical_weights\"][0]\n",
    ")\n",
    "lexical_scores2 = bge_flagmodel.compute_lexical_matching_score(\n",
    "    bge_encoded[\"lexical_weights\"][0], bge_encoded[\"lexical_weights\"][1]\n",
    ")\n",
    "# 0 <-> 0\n",
    "print(lexical_scores1)\n",
    "# 0 <-> 1\n",
    "print(lexical_scores2)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Multi-Vector (ColBERT)\n",
    "\n",
    "ColBERT(Contextualized Late Interaction over BERT)는 문서 검색을 위한 효율적인 방법입니다. 이 방식은 문서와 쿼리를 여러 개의 벡터로 표현하는 multi-vector 접근법을 사용합니다.\n",
    "\n",
    "**작동 방식**\n",
    "\n",
    "1. 문서의 각 토큰에 대해 별도의 벡터를 생성합니다. 즉, 하나의 문서는 여러 개의 벡터로 표현됩니다.\n",
    "2. 쿼리도 마찬가지로 각 토큰에 대해 별도의 벡터를 생성합니다.\n",
    "3. 검색 시, 쿼리의 각 토큰 벡터와 문서의 모든 토큰 벡터 사이의 유사도를 계산합니다.\n",
    "4. 이 유사도들을 종합하여 최종 검색 점수를 계산합니다.\n",
    "\n",
    "**장점**\n",
    "- 토큰 수준의 세밀한 매칭이 가능합니다.\n",
    "- 문맥을 고려한 임베딩을 생성할 수 있습니다.\n",
    "- 긴 문서에 대해서도 효과적으로 작동합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bge_flagmodel = BGEM3FlagModel(\n",
    "    \"BAAI/bge-m3\", use_fp16=True\n",
    ")  # use_fp16을 True로 설정하면 약간의 성능 저하와 함께 계산 속도가 빨라집니다.\n",
    "bge_encoded = bge_flagmodel.encode(texts, return_colbert_vecs=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "colbert_scores1 = bge_flagmodel.colbert_score(\n",
    "    bge_encoded[\"colbert_vecs\"][0], bge_encoded[\"colbert_vecs\"][0]\n",
    ")\n",
    "colbert_scores2 = bge_flagmodel.colbert_score(\n",
    "    bge_encoded[\"colbert_vecs\"][0], bge_encoded[\"colbert_vecs\"][1]\n",
    ")\n",
    "# 0 <-> 0\n",
    "print(colbert_scores1)\n",
    "# 0 <-> 1\n",
    "print(colbert_scores2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
